/// <reference path="Libraries/jasmine/jasmine.d.ts" />
/// <reference path="Core.ts" />

/**
 * Matchers for Jasmine.
 */
module ExtSpec.Jasmine.Matchers {
    'use strict';

    /**
     * The configuration structure for a listened expectation.
     */
    export interface IListenedConfig {
        /**
         * The event name to match.
         */
        event: string;
        /**
         * The listener to match.
         */
        listener?: Function;
    }

    /**
     * The configuration structure for a controlled expectation.
     */
    export interface IControlledConfig extends IListenedConfig {
        /**
         * The string or regular expression selector to match.
         */
        selector?: any;
    }

    interface IListenedMatcherConfig {
        actual: jasmine.Spy;
        expected: any;
        isManaged: boolean;
    }

    /**
     * Test if the actual value contains a name of the given type, and sets an appropriate message if it is not found.
     * @param {Object} actual The actual value to test.
     * @param {String} name The name to find in the actual value.
     * @param {String} type A single character representing the type to find.
     * @returns {Boolean}
     */
    function hasItem(actual: any, name: string, type: string): boolean {
        var config: ResourceConfig = ExtSpec.getResourceConfig(type),
            list: any[] = actual && actual[config.property];

        return config.matcher(list, name);
    }

    /**
     * Tests if the expectations's actual value extends or overrides a specific class.
     * @param {jasmine.ExpectationResult} expectation The expectation to test.
     * @param {String} name The class name to match against the actual value.
     * @param {String} property The property name to match against the name.
     * @returns {Boolean}
     */
    function isExtension(actual: any, name: string, property: string): boolean {
        if (ExtSpec.isTypeOf(actual, 'fn')) {
            actual = actual.prototype;
        }

        return actual && actual[property] === name;
    }

    /**
     * Tests if a spy's arguments match the provided event configuration.
     * @param {Object} config The event configuration.
     * @returns {Boolean}
     */
    function listenedMatcher(config: IListenedMatcherConfig): boolean {
        if (!(config.actual && config.actual.and)) {
            throw new Error('Expected ' + jasmine.pp(config.actual) + ' to be a spy.');
        }

        var actual: jasmine.Spy = config.actual,
            key: string,
            eventMap: any,
            listener: any,
            expected: IListenedConfig,
            i: number,
            len: number,
            args: any[],
            arg: number = config.isManaged ? 1 : 0;

        if (!ExtSpec.isTypeOf(config.expected, 'st')) {
            expected = config.expected;
        } else {
            expected = {
                event: config.expected
            };
        }

        for (i = 0, len = actual.calls.count(); i < len; i += 1) {
            args = actual.calls.argsFor(i);
            eventMap = args[arg];

            if (ExtSpec.isTypeOf(eventMap, 'st')) {
                key = eventMap;
                eventMap = {};
                eventMap[key] = args[arg + 1];
            }

            if (eventMap.hasOwnProperty(expected.event)) {
                if (!expected.listener) {
                    return true;
                }

                listener = eventMap[expected.event];

                if ((listener.fn || listener) === expected.listener) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Passes the expectation if the given model name is found.
     * @param {String} modelName The model name to find.
     * @returns {Boolean}
     */
    export function toHaveModel(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'md')
                };
            }
        };
    }

    /**
     * Passes if the given view name is found.
     * @param {String} viewName The view name to find.
     * @returns {Boolean}
     */
    export function toHaveView(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'vw')
                };
            }
        };
    }

    /**
     * Passes if the given controller name is found.
     * @param {String} controllerName The controller name to find.
     * @returns {Boolean}
     */
    export function toHaveController(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'ct')
                };
            }
        };
    }

    /**
     * Passes if the given store name is found.
     * @param {String} storeName The store name to find.
     * @returns {Boolean}
     */
    export function toHaveStore(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'st')
                };
            }
        };
    }

    /**
     * Passes if the given ref name is found.
     * @param {String} refName The ref name to find.
     * @returns {Boolean}
     */
    export function toHaveRef(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'rf')
                };
            }
        };
    }

    /**
     * Passes if the given config name is found.
     * @param {String} configName The config name to find.
     * @returns {Boolean}
     */
    export function toHaveConfig(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'cf')
                };
            }
        };
    }

    /**
     * Passes if the given class name is required.
     * @param {String} refName The required class name to find.
     * @returns {Boolean}
     */
    export function toRequire(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: hasItem(actual, expected, 'rq')
                };
            }
        };
    }

    /**
     * Passes if the class instance or constructor extends the given class name.
     * @param {String} className The class name to match.
     * @returns {Boolean}
     */
    export function toExtend(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: isExtension(actual, expected, 'extend')
                };
            }
        };
    }

    /**
     * Passes if the class instance or constructor overrides the given class name.
     * @param {String} className The class name to match.
     * @returns {Boolean}
     */
    export function toOverride(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: isExtension(actual, expected, 'override')
                };
            }
        };
    }

    /**
     * Passes if the keys in the expected object have matching values in a spy argument.
     * @param {Object} expected The object containing keys and values to match.
     * @param {Number} [arg] The zero based argument index to compare. Defaults to zero (the first).
     * @returns {Boolean}
     */
    export function toHaveBeenCalledWithConfig(): any {
        return {
            compare: function (actual: any, expected: any, arg?: number): any {
                var i: number,
                    len: number,
                    spy: jasmine.Spy = actual,
                    config: any,
                    pass: boolean = false;

                if (!ExtSpec.isTypeOf(arg, 'nm')) {
                    arg = 0;
                }

                if (jasmine.isSpy(spy)) {
                    for (i = 0, len = spy.calls.count(); i < len; i += 1) {
                        config = spy.calls.argsFor(i)[arg];

                        if (config && ExtSpec.objectKeysMatch(config, expected)) {
                            pass = true;
                            break;
                        }
                    }
                }

                return {
                    pass: pass
                };
            }
        };
    }

    /**
     * Passes if the spy added a listener for the given event.
     * @param {String|Object} expected The event name or configuration to match.
     * @returns {Boolean}
     */
    export function toHaveAddedListener(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: listenedMatcher({
                        actual: actual,
                        expected: expected,
                        isManaged: false
                    })
                };
            }
        };
    }

    /**
     * Passes if the spy removed a listener for the given event.
     * @param {String|Object} expected The event name or configuration to match.
     * @returns {Boolean}
     */
    export function toHaveRemovedListener(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: listenedMatcher({
                        actual: actual,
                        expected: expected,
                        isManaged: false
                    })
                };
            }
        };
    }

    /**
     * Passes if the spy added a listener for the given event.
     * @param {String|Object} expected The event name or configuration to match.
     * @returns {Boolean}
     */
    export function toHaveAddedManagedListener(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: listenedMatcher({
                        actual: actual,
                        expected: expected,
                        isManaged: true
                    })
                };
            }
        };
    }

    /**
     * Passes if the spy removed a listener for the given event.
     * @param {String|Object} expected The event name or configuration to match.
     * @returns {Boolean}
     */
    export function toHaveRemovedManagedListener(): any {
        return {
            compare: function (actual: any, expected: any): any {
                return {
                    pass: listenedMatcher({
                        actual: actual,
                        expected: expected,
                        isManaged: true
                    })
                };
            }
        };
    }

    /**
     * Passes if the spy controlled the given event.
     * @param {String|Object} expected The event name or configuration to match.
     * @returns {Boolean}
     */
    export function toHaveControlled(): any {
        return {
            compare: function (actual: any, expected: any) {
                if (!jasmine.isSpy(actual)) {
                    throw new Error('Expected ' + jasmine.pp(actual) + ' to be a spy.');
                }

                var selector: string,
                    selectorMap: any,
                    selectorPattern: RegExp,
                    args: any[],
                    config: IControlledConfig,
                    listener: any,
                    i: number,
                    len: number,
                    pass: boolean = true;

                if (!ExtSpec.isTypeOf(expected, 'st')) {
                    config = expected;
                } else {
                    config = {
                        event: expected
                    };
                }

                if (ExtSpec.isTypeOf(config.selector, 'rg')) {
                    selectorPattern = config.selector;
                } else {
                    selectorPattern = new RegExp(ExtSpec.regExpEscape(config.selector || ''));
                }

                for (i = 0, len = actual.calls.count(); i < len; i += 1) {
                    args = actual.calls.argsFor(i);
                    selectorMap = args[0];

                    if (ExtSpec.isTypeOf(selectorMap, 'st')) {
                        selector = selectorMap;
                        selectorMap = {};
                        selectorMap[selector] = args[1];
                    }

                    for (selector in selectorMap) {
                        if (selectorMap.hasOwnProperty(selector) && selectorPattern.test(selector) && selectorMap[selector].hasOwnProperty(config.event)) {
                            if (!config.listener) {
                                pass = true;
                                break;
                            }

                            listener = selectorMap[selector][config.event];

                            if ((listener.fn || listener) === config.listener) {
                                pass = true;
                                break;
                            }
                        }
                    }
                }

                return {
                    pass: pass
                };
            }
        };
    }
}